// This file was only slightly adapted from last still image encoding practicum
#include "Qik.h"
#include "DoubleMatrix.h"
#include "ByteStream.h"
#include "RunLengthDecoder.h"
#include "RunLengthEncoder.h"

static const double defaultQuantizationTable4x4[16]=
{
	1,		2,		3,		4,
	2,		3,		4,		6,
	3,		4,		6,		8,
	4,		6,		8,		10,
};

const char *Qik::VERSIONHEADER = "QIK1";

const double Qik::defaultCompressionFactor = 1;
DoubleMatrix Qik::defaultQuantizationTable(4, 4, defaultQuantizationTable4x4);

Qik::Qik(bool isstripped): logfile(0), stripped(isstripped)
{
	SetCompressionFactor(defaultCompressionFactor);
}

Qik::Qik(unsigned int newWidth, unsigned int newHeight): Image(newWidth, newHeight), logfile(0)
{
	SetCompressionFactor(defaultCompressionFactor);
}

Qik::~Qik()
{
	StopLogging();
}

void Qik::StartLogging(const char *filename)
{
	logfile = new std::ofstream(filename);
	if (logfile == 0 || !logfile->good())
		throw "Qik: Cannot open log file for writing";
}

bool Qik::Logging() const
{
	return logfile != 0;
}

std::ofstream& Qik::Log()
{
	if (logfile == 0)
		throw "Qik: No logfile opened";

	return *logfile;
}

void Qik::StopLogging()
{
	if (logfile)
	{
		delete logfile;
		logfile = 0;
	}
}

void Qik::ReadHeaderFromStream(ByteStream& stream)
{
	char versionString[5];
	unsigned int width, height;


	memset(versionString, 0, sizeof(versionString));
	stream.ReadRaw(versionString, 4);

	if (strcmp(versionString, VERSIONHEADER))
		throw "Qik: Reading invalid qik format";


	stream.ReadRaw(&width, sizeof(width));
	stream.ReadRaw(&height, sizeof(height));
	stream.ReadRaw(&compressionFactor, sizeof(compressionFactor));

	SetDimension(width, height);
}

void Qik::DecodeMatrix(DoubleMatrix& Y, const DoubleMatrix& quantizationTable, bool sublogging)
{
	Y.ZigzagInv();


	if (sublogging && Logging())
	{
		Log() << "  Before-dequantization: " << endl;
		Log() << Y;
		Log() << endl;
	}


	// Dequantize Y
	Y.MultiplyElementsBy( quantizationTable );

	if (sublogging && Logging())
	{
		Log() << "  Before-IDCT: " << endl;
		Log() << Y;
		Log() << endl;
	}


	// Inverse DCT
	Y.InverseDCT();

	if (sublogging && Logging())
	{
		Log() << "  Raw: " << endl;
		Log() << Y;
		Log() << endl;
	}


	// Export matrix data to image block
	Y.Round();
	Y.Clip(0, 255);
}

void Qik::ReadFromStream(ByteStream& stream)
{
	signed char dctBlock[BLOCKW * BLOCKH];
	unsigned char rawBlock[BLOCKW * BLOCKH];
	DoubleMatrix Y(BLOCKH, BLOCKW);
	DoubleMatrix quantizationTable = defaultQuantizationTable;


	if (!stripped)
		ReadHeaderFromStream(stream);

	if (Width() == 0 || Height() == 0)
		throw "Qik: Invalid dimension";
	if ((Width() % BLOCKW) || (Height() % BLOCKH))
		throw "Qik: Segmentation with invalid dimensions";
	
	quantizationTable.Scale(compressionFactor);

	// Initialize segmentation system, 4x4 block
	InitializeSegment(BLOCKW, BLOCKH);


	// For each block
	while(1)
	{
		ImageSegment *seg = NextSegment();

		if (!seg)
			break;

		if (Logging())
		{
			Log() << "Decoding block (" << seg->OriginX() << "," << seg->OriginY() << "):" << endl;
		}


		// Runlength decode a block
		stream.Read(dctBlock, sizeof(dctBlock));

		
		// Import quantized dct data into matrix
		Y.ImportSignedChars( dctBlock );

		DecodeMatrix(Y, quantizationTable, true);

		Y.ExportUnsignedChars( rawBlock );

		seg->ImportRaw(rawBlock, sizeof(rawBlock));
	}
}

void Qik::ReadFromFile(const char* filename)
{
	RunLengthDecoder stream;


	stream.ReadFromFile(filename);

	ReadFromStream(stream);
}

void Qik::WriteHeaderToStream(ByteStream& stream)
{
	unsigned int width = Width();
	unsigned int height = Height();

	// Write QIK file header
	stream.WriteRaw(VERSIONHEADER, 4);
	stream.WriteRaw(&width, sizeof(width));
	stream.WriteRaw(&height, sizeof(height));
	stream.WriteRaw(&compressionFactor, sizeof(compressionFactor));
}

void Qik::EncodeMatrix(DoubleMatrix& X, const DoubleMatrix& quantizationTable, bool sublogging)
{
	if (sublogging && Logging())
	{
		Log() << "  Raw: " << endl;
		Log() << X;
		Log() << endl;
	}

	// Forward DCT on block
	X.ForwardDCT();

	if (sublogging && Logging())
	{
		Log() << "  After-DCT: " << endl;
		Log() << X;
		Log() << endl;
	}


	// Quantize X
	X.DivideElementsBy( quantizationTable );

	if (sublogging && Logging())
	{
		Log() << "  After-quantization: " << endl;
		Log() << X;
		Log() << endl;
	}


	X.Zigzag();

	// Round off to the proper integers in the range -128..127, export the matrix's final data
	X.Round();
	X.Clip(-128, 127);
}

void Qik::EnableWriteback()
{
	encodewriteback = true;
}

void Qik::WriteToStream(ByteStream& stream)
{
	signed char dctBlock[BLOCKW * BLOCKH];
	unsigned char rawBlock[BLOCKW * BLOCKH];
	DoubleMatrix X(BLOCKH, BLOCKW);
	DoubleMatrix quantizationTable = defaultQuantizationTable;

	// After DCT, the AC values can be in the range -1024..1023, DC in range 0..1020
	// So, make sure the scale is atleast 8, to make sure after quantization, the AC values lie in the range -128..127, DC in 0..127

	if (compressionFactor < 8)
		throw "Qik: Internal compressionfactor must be 8 or higher";
	if (Width() == 0 || Height() == 0)
		throw "Qik: Invalid dimension";
	if ((Width() % BLOCKW) || (Height() % BLOCKH))
		throw "Qik: Segmentation with invalid dimensions";
	
	quantizationTable.Scale(compressionFactor);

	// Initialize segmentation system, 4x4 block
	InitializeSegment(BLOCKW, BLOCKH);

	if (!stripped)
		WriteHeaderToStream(stream);


	// For each block
	while(1)
	{
		// Get next image block
		ImageSegment *seg = NextSegment();

		

		if (!seg)
			break;

		if (Logging())
		{
			Log() << "Encoding block (" << seg->OriginX() << "," << seg->OriginY() << "):" << endl;
		}

		seg->ExportRaw(rawBlock, sizeof(rawBlock));

		// Import image block data into matrix
		X.ImportUnsignedChars( rawBlock );

		EncodeMatrix(X, quantizationTable, true);

		X.ExportSignedChars( dctBlock );


		// Runlength encode quantized dct data
		stream.Write(dctBlock, sizeof(dctBlock));

		if (encodewriteback)
		{
			// Update image with decoded result
			X.ImportSignedChars( dctBlock );

			DecodeMatrix(X, quantizationTable, true);

			X.ExportUnsignedChars( rawBlock );

			seg->ImportRaw(rawBlock, sizeof(rawBlock));
		}
	}
}

void Qik::WriteToFile(const char* filename)
{
	RunLengthEncoder stream;

	WriteToStream(stream);

	stream.WriteToFile(filename);
}

void Qik::SetCompressionFactor(double factor)
{
	if (factor < 0)
		factor = 0;

	// Compression factor must be atleast 8

	compressionFactor = factor + 8;
}


